<!doctype html>
<meta charset=utf-8>
<script src="/resources/testharness.js"></script>
<script src="/resources/testharnessreport.js"></script>
<script>
'use strict';

['audio', 'video'].forEach((kind) => {
  promise_test(async t => {
    const pc1 = new RTCPeerConnection();
    t.add_cleanup(() => pc1.close());
    const pc2 = new RTCPeerConnection();
    t.add_cleanup(() => pc2.close());

    const transceiver = pc1.addTransceiver(kind);

    // Complete O/A exchange such that the transceiver gets associated.
    await pc1.setLocalDescription();
    await pc2.setRemoteDescription(pc1.localDescription);
    await pc2.setLocalDescription();
    await pc1.setRemoteDescription(pc2.localDescription);
    assert_not_equals(transceiver.mid, null, 'mid before stop()');
    assert_not_equals(transceiver.direction, 'stopped',
                      'direction before stop()');
    assert_not_equals(transceiver.currentDirection, 'stopped',
                      'currentDirection before stop()');

    // Stop makes it stopping, but not stopped.
    transceiver.stop();
    assert_not_equals(transceiver.mid, null, 'mid after stop()');
    assert_equals(transceiver.direction, 'stopped', 'direction after stop()');
    assert_not_equals(transceiver.currentDirection, 'stopped',
                      'currentDirection after stop()');

    // Negotiating makes it stopped.
    await pc1.setLocalDescription();
    await pc2.setRemoteDescription(pc1.localDescription);
    await pc2.setLocalDescription();
    await pc1.setRemoteDescription(pc2.localDescription);
    assert_equals(transceiver.mid, null, 'mid after negotiation');
    assert_equals(transceiver.direction, 'stopped',
                  'direction after negotiation');
    assert_equals(transceiver.currentDirection, 'stopped',
                  'currentDirection after negotiation');
  }, `[${kind}] Locally stopped transceiver goes from stopping to stopped`);

  promise_test(async t => {
    const pc = new RTCPeerConnection();
    t.add_cleanup(() => pc.close());

    const transceiver = pc.addTransceiver(kind);
    const trackEnded = new Promise(r => transceiver.receiver.track.onended = r);
    assert_equals(transceiver.receiver.track.readyState, 'live');
    transceiver.stop();
    // Stopping triggers ending the track, but this happens asynchronously.
    assert_equals(transceiver.receiver.track.readyState, 'live');
    await trackEnded;
    assert_equals(transceiver.receiver.track.readyState, 'ended');
  }, `[${kind}] Locally stopping a transceiver ends the track`);

  promise_test(async t => {
    const pc1 = new RTCPeerConnection();
    t.add_cleanup(() => pc1.close());
    const pc2 = new RTCPeerConnection();
    t.add_cleanup(() => pc2.close());

    const pc1Transceiver = pc1.addTransceiver(kind);
    await pc1.setLocalDescription();
    await pc2.setRemoteDescription(pc1.localDescription);
    await pc2.setLocalDescription();
    await pc1.setRemoteDescription(pc2.localDescription);
    const [pc2Transceiver] = pc2.getTransceivers();

    pc1Transceiver.stop();

    await pc1.setLocalDescription();
    assert_equals(pc2Transceiver.receiver.track.readyState, 'live');
    // Applying the remote offer immediately ends the track, we don't need to
    // create or apply an answer.
    await pc2.setRemoteDescription(pc1.localDescription);
    // sRD just resolved, so we're in the success task for sRD. The transition
    // from live -> ended is queued right now.
    assert_equals(pc2Transceiver.receiver.track.readyState, 'live');
    await new Promise(r => pc2Transceiver.receiver.track.onended = r);
    assert_equals(pc2Transceiver.receiver.track.readyState, 'ended');
  }, `[${kind}] Remotely stopping a transceiver ends the track`);

  promise_test(async t => {
    const pc1 = new RTCPeerConnection();
    t.add_cleanup(() => pc1.close());
    const pc2 = new RTCPeerConnection();
    t.add_cleanup(() => pc2.close());

    const pc1Transceiver = pc1.addTransceiver(kind);

    // Complete O/A exchange such that the transceiver gets associated.
    await pc1.setLocalDescription();
    await pc2.setRemoteDescription(pc1.localDescription);
    await pc2.setLocalDescription();
    await pc1.setRemoteDescription(pc2.localDescription);
    const [pc2Transceiver] = pc2.getTransceivers();
    assert_not_equals(pc2Transceiver.mid, null, 'mid before stop()');
    assert_not_equals(pc2Transceiver.direction, 'stopped',
                      'direction before stop()');
    assert_not_equals(pc2Transceiver.currentDirection, 'stopped',
                      'currentDirection before stop()');

    // Make the remote transceiver stopped.
    pc1Transceiver.stop();

    // Negotiating makes it stopped.
    assert_equals(pc2.getTransceivers().length, 1);
    await pc1.setLocalDescription();
    await pc2.setRemoteDescription(pc1.localDescription);
    // As soon as the remote offer is set, the transceiver is stopped but it is
    // not disassociated or removed until setting the local answer.
    assert_equals(pc2.getTransceivers().length, 1);
    assert_not_equals(pc2Transceiver.mid, null, 'mid during negotiation');
    assert_equals(pc2Transceiver.direction, 'stopped',
                  'direction during negotiation');
    assert_equals(pc2Transceiver.currentDirection, 'stopped',
                  'currentDirection during negotiation');
    await pc2.setLocalDescription();
    assert_equals(pc2.getTransceivers().length, 0);
    assert_equals(pc2Transceiver.mid, null, 'mid after negotiation');
    assert_equals(pc2Transceiver.direction, 'stopped',
                  'direction after negotiation');
    assert_equals(pc2Transceiver.currentDirection, 'stopped',
                  'currentDirection after negotiation');
  }, `[${kind}] Remotely stopped transceiver goes directly to stopped`);

  promise_test(async t => {
    const pc = new RTCPeerConnection();
    t.add_cleanup(() => pc.close());

    const transceiver = pc.addTransceiver(kind);

    // Rollback does not end the track, because the transceiver is not removed.
    await pc.setLocalDescription();
    await pc.setLocalDescription({type:'rollback'});
    assert_equals(transceiver.receiver.track.readyState, 'live');
  }, `[${kind}] Rollback when transceiver is not removed does not end track`);

  promise_test(async t => {
    const pc1 = new RTCPeerConnection();
    t.add_cleanup(() => pc1.close());
    const pc2 = new RTCPeerConnection();
    t.add_cleanup(() => pc2.close());

    const pc1Transceiver = pc1.addTransceiver(kind);

    // Start negotiation, causing a transceiver to be created.
    await pc1.setLocalDescription();
    await pc2.setRemoteDescription(pc1.localDescription);
    const [pc2Transceiver] = pc2.getTransceivers();

    // Rollback such that the transceiver is removed.
    await pc2.setRemoteDescription({type:'rollback'});
    assert_equals(pc2.getTransceivers().length, 0);
    // sRD just resolved, so we're in the success task for sRD. The transition
    // from live -> ended is queued right now.
    assert_equals(pc2Transceiver.receiver.track.readyState, 'live');
    await new Promise(r => pc2Transceiver.receiver.track.onended = r);
    assert_equals(pc2Transceiver.receiver.track.readyState, 'ended');
  }, `[${kind}] Rollback when removing transceiver does end the track`);

  // Same test as above but looking at direction and currentDirection.
  promise_test(async t => {
    const pc1 = new RTCPeerConnection();
    t.add_cleanup(() => pc1.close());
    const pc2 = new RTCPeerConnection();
    t.add_cleanup(() => pc2.close());

    const pc1Transceiver = pc1.addTransceiver(kind);

    // Start negotiation, causing a transceiver to be created.
    await pc1.setLocalDescription();
    await pc2.setRemoteDescription(pc1.localDescription);
    const [pc2Transceiver] = pc2.getTransceivers();

    // Rollback such that the transceiver is removed.
    await pc2.setRemoteDescription({type:'rollback'});
    assert_equals(pc2.getTransceivers().length, 0);
    // The removed transceiver is stopped.
    assert_equals(pc2Transceiver.currentDirection, 'stopped',
                  'currentDirection indicate stopped');
    // A stopped transceiver is necessarily also stopping.
    assert_equals(pc2Transceiver.direction, 'stopped',
                  'direction indicate stopping');
    // A stopped transceiver has no mid.
    assert_equals(pc2Transceiver.mid, null, 'not associated');
  }, `[${kind}] Rollback when removing transceiver makes it stopped`);

  promise_test(async t => {
    const pc1 = new RTCPeerConnection();
    t.add_cleanup(() => pc1.close());
    const pc2 = new RTCPeerConnection();
    t.add_cleanup(() => pc2.close());

    const constraints = {};
    constraints[kind] = true;
    const stream = await navigator.mediaDevices.getUserMedia(constraints);
    const [track] = stream.getTracks();

    pc1.addTrack(track);
    pc2.addTrack(track);
    const transceiver = pc2.getTransceivers()[0];

    const ontrackEvent = new Promise(r => {
      pc2.ontrack = e => r(e.track);
    });

    // Simulate glare: both peer connections set local offers.
    await pc1.setLocalDescription();
    await pc2.setLocalDescription();
    // Set remote offer, which implicitly rolls back the local offer. Because
    // `transceiver` is an addTrack-transceiver, it should get repurposed.
    await pc2.setRemoteDescription(pc1.localDescription);
    assert_equals(transceiver.receiver.track.readyState, 'live');
    // Sanity check: the track should still be live when ontrack fires.
    assert_equals((await ontrackEvent).readyState, 'live');
  }, `[${kind}] Glare when transceiver is not removed does not end track`);
});
</script>
